﻿using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using RedLockNet.SERedis;
using RedLockNet.SERedis.Configuration;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Tsual.Util;
using Tsual.Util.Redis;
using Tsual.Wechat.Cache;
using Tsual.Wechat.Message;
using Tsual.Wechat.Message.Exceptions;
using Tsual.Wechat.Message.Impl;
using Tsual.Wechat.Message.Impl.Internal;
using Tsual.Wechat.Message.Impl.Response;
using Tsual.Wechat.Message.Interface.Request;
using Tsual.Wechat.Message.Interface.Response;
using System.Web;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Mvc;
using System.Xml.Linq;
using System.Net;
using System.Collections;
using Tsual.Wechat;
using Tsual.Wechat.Formatters;
using Microsoft.AspNetCore.Hosting;
using Tsual.Wechat.Enums;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Tsual.Wechat.Filters;
using System.Reflection;
using Tsual.Wechat.Controllers;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.Options;
namespace Tsual.Wechat.Formatters
{
    internal class WechatMessageEncryptedFeature
    {
        public string RequestTimespan { get; set; }
        public string RequestNonce { get; set; }
    }

    public class WechatMessageInputFormatter : InputFormatter
    {
        ILogger _logger;
        ILogger logger => _logger != null ? _logger : _logger = WechatContext._Instance.serviceProvider?.GetService<ILogger<WechatMessageInputFormatter>>();
        WechatBizMsgCrypt wechatEncrypter => WechatContext.Instance.BizMsgCrypt;

        public WechatMessageInputFormatter()
        {
            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
        }

        protected override bool CanReadType(Type type) => type.GetInterfaces().Contains(typeof(IWechatRequestMessage)) ? base.CanReadType(type) : false;

        public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            try
            {
                if (context.HttpContext.Request.Body.CanSeek)
                    context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
                var xmlContent = new StreamReader(context.HttpContext.Request.Body).ReadToEnd();
                logger?.LogTrace($"微信Format原报文{xmlContent}");
                var doc = new XmlDocument();
                doc.LoadXml(xmlContent);

                //check for encrypt
                for (int i = 0; i < doc.FirstChild.ChildNodes.Count; i++)
                {
                    XmlElement node = doc.FirstChild.ChildNodes[i] as XmlElement;

                    if (node == null) continue;

                    if (node.Name == "Encrypt")
                    {
                        if (wechatEncrypter == null)
                        {
                            throw new ArgumentNullException("发现了加密微信报文，但没有配置加密密钥");
                        }

                        var weenMeta = new WechatMessageEncryptedFeature()
                        {
                            RequestNonce = context.HttpContext.Request.Query["nonce"],
                            RequestTimespan = context.HttpContext.Request.Query["timestamp"]
                        };
                        context.HttpContext.Features.Set(weenMeta);
                        //dec and reload xml
                        wechatEncrypter.DecryptMsg(
                            context.HttpContext.Request.Query["msg_signature"],
                            weenMeta.RequestTimespan,
                            weenMeta.RequestNonce,
                            xmlContent,
                            ref xmlContent);
                        doc = new XmlDocument();
                        doc.LoadXml(xmlContent);
                        logger?.LogTrace($"微信报文解密后{xmlContent}");

                        break;
                    }

                }

                //deserialize object
                var rtn_object = new WechatMessageRequestModel();
                var fields = rtn_object.GetType().GetProperties();
                foreach (XmlElement node in doc.FirstChild.ChildNodes)
                {
                    var field = fields.Where(x => x.Name == node.Name).FirstOrDefault();
                    if (field != null)
                    {
                        field.SetValue(rtn_object, _ProduceText(field.PropertyType, node.InnerText));
                    }
                }
                return InputFormatterResult.SuccessAsync(rtn_object);
            }
            catch (Exception)
            {
                return InputFormatterResult.FailureAsync();
            }

        }

        private static readonly Dictionary<Type, Func<string, object>> TextProducers = new Dictionary<Type, Func<string, object>>()
        {
            { typeof(string),str=>str },
            { typeof(int),str=>int.Parse(str) },
            { typeof(ulong),str=>ulong.Parse(str) },
            { typeof(double),str=>double.Parse(str) },
        };
        private static object _ProduceText(Type type, string txt) => TextProducers.ContainsKey(type) ? TextProducers[type](txt) : null;
    }

    public class WechatMessageOutputFormatter : OutputFormatter
    {
        static readonly Dictionary<Type, Action<XmlElement, XmlDocument, object>> _SupportInterfacesMapping = new Dictionary<Type, Action<XmlElement, XmlDocument, object>>()
        {
            { typeof(IWechatResponseText),(el,doc,obj)=>{
                var wobj=obj as IWechatResponseText;
                WriteResponseMessage(wobj,WechatReponseMsgTypes.text,el,doc);
                WriteXmlElement(el,doc,"Content",wobj.Content);
            } },
            { typeof(IWechatResponseImage),(el,doc,obj)=>{
                var wobj=obj as IWechatResponseImage;
                WriteResponseMessage(wobj,WechatReponseMsgTypes.text,el,doc);
                var sel= doc.CreateElement("Image");
                el.AppendChild(sel);
                WriteXmlElement(sel,doc,"MediaId",wobj.MediaId);
            } },
            { typeof(IWechatResponseVoice),(el,doc,obj)=>{
                var wobj=obj as IWechatResponseVoice;
                WriteResponseMessage(wobj,WechatReponseMsgTypes.text,el,doc);
                var sel= doc.CreateElement("Voice");
                el.AppendChild(sel);
                WriteXmlElement(sel,doc,"MediaId",wobj.MediaId);
            } },
            { typeof(IWechatResponseVideo),(el,doc,obj)=>{
                var wobj=obj as IWechatResponseVideo;
                WriteResponseMessage(wobj,WechatReponseMsgTypes.text,el,doc);
                var sel= doc.CreateElement("Video");
                el.AppendChild(sel);
                WriteXmlElement(sel,doc,"MediaId",wobj.MediaId);
                WriteXmlElement(sel,doc,"Title",wobj.Title);
                WriteXmlElement(sel,doc,"Description",wobj.Description);
            } },
            { typeof(IWechatResponseMusic),(el,doc,obj)=>{
                var wobj=obj as IWechatResponseMusic;
                WriteResponseMessage(wobj,WechatReponseMsgTypes.text,el,doc);
                var sel= doc.CreateElement("Music");
                el.AppendChild(sel);
                WriteXmlElement(sel,doc,"Title",wobj.Title);
                WriteXmlElement(sel,doc,"Description",wobj.Description);
                WriteXmlElement(sel,doc,"MusicUrl",wobj.MusicUrl);
                WriteXmlElement(sel,doc,"HQMusicUrl",wobj.HQMusicUrl);
                WriteXmlElement(sel,doc,"ThumbMediaId",wobj.ThumbMediaId);
            } },
            { typeof(IWechatResponseNews),(el,doc,obj)=>{
                var wobj=obj as IWechatResponseNews;
                WriteResponseMessage(wobj,WechatReponseMsgTypes.text,el,doc);
                var sel= doc.CreateElement("Articles");
                el.AppendChild(sel);
                WriteXmlElement(sel,doc,"ArticleCount",wobj.ArticleCount.ToString());
                if(wobj.Articles!=null)
                {
                    foreach(var Article in wobj.Articles)
                    {
                        var tel= doc.CreateElement("item");
                        sel.AppendChild(tel);
                        WriteXmlElement(tel,doc,"Description",Article.Description);
                        WriteXmlElement(tel,doc,"Title",Article.Title);
                        WriteXmlElement(tel,doc,"PicUrl",Article.PicUrl);
                        WriteXmlElement(tel,doc,"Url",Article.Url);
                    }
                }

            } },
        };


        ILogger _logger;
        ILogger logger => _logger != null ? _logger : _logger = WechatContext._Instance.serviceProvider?.GetService<ILogger<WechatMessageOutputFormatter>>();
        WechatBizMsgCrypt wechatEncrypter => WechatContext.Instance.BizMsgCrypt;

        public WechatMessageOutputFormatter()
        {
            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
        }

        protected override bool CanWriteType(Type type)
            => (
                (type.IsInterface && (_SupportInterfacesMapping.ContainsKey(type) || typeof(IWechatMessageHandleResult).IsAssignableFrom(type)))
                || type.GetInterfaces().Count(x => _SupportInterfacesMapping.ContainsKey(x)) == 1
            )
            ? base.CanWriteType(type) : false;

        public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
        {
            var @Target = context.Object;

            if (@Target == null)
            {
                context.HttpContext.Response.StatusCode = 200;
                return;
            }

            context.HttpContext.Response.ContentType = "text/xml";
            var doc = new XmlDocument();
            var head = doc.CreateElement("xml");
            doc.AppendChild(head);
            _SupportInterfacesMapping[@Target.GetType().GetInterfaces()
                .Where(x => _SupportInterfacesMapping.ContainsKey(x))
                .First()]?.Invoke(head, doc, @Target);

            context.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK;
            var weenMeta = context.HttpContext.Features.Get<WechatMessageEncryptedFeature>();
            if (weenMeta != null)
            {
                logger?.LogTrace($"已发现了微信加密源信息，使用密文返回");

                if (wechatEncrypter == null)
                {
                    throw new ArgumentNullException("发现了微信加密源信息，但没有配置加密密钥");
                }

                var ms = new MemoryStream();
                doc.Save(ms);
                var ol = new StreamReader(ms).ReadToEnd();
                var olt = "";
                wechatEncrypter.EncryptMsg(ol, weenMeta.RequestTimespan, weenMeta.RequestNonce, ref olt);
                var sr = new StreamWriter(context.HttpContext.Response.Body);
                sr.Write(olt);
                await sr.FlushAsync();
            }
            else
            {
                logger?.LogTrace($"没有发现加密源信息，使用明文返回");
                doc.Save(context.HttpContext.Response.Body);
                await context.HttpContext.Response.Body.FlushAsync();
            }
        }

        private static void WriteResponseMessage(IWechatResponseMessage msg, WechatReponseMsgTypes type, XmlElement el, XmlDocument doc)
        {
            WriteXmlElement(el, doc, "MsgType", type.ToString());
            WriteXmlElement(el, doc, "ToUserName", msg.ToUserName);
            WriteXmlElement(el, doc, "FromUserName", msg.FromUserName);
            WriteXmlElement(el, doc, "CreateTime", msg.CreateTime.ToString());
        }

        private static void WriteXmlElement(XmlElement el, XmlDocument doc, string name, string value)
        {
            var mte = doc.CreateElement(name);
            mte.InnerText = value;
            el.AppendChild(mte);
        }
    }

}

namespace Tsual.Wechat.Filters
{
    public class WechatServerAddressFilter : ActionFilterAttribute
    {
        private ILogger<WechatServerAddressFilter> logger;
        private IDatabase cache;
        private RedLockFactory lockFactory;
        private IHttpClientFactory httpClientFactory;



        private readonly static ReaderWriterLockSlim slim = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

        public WechatServerAddressFilter(ILogger<WechatServerAddressFilter> logger, IDatabase cache, RedLockFactory lockFactory, IHttpClientFactory httpClientFactory)
        {
            this.logger = logger;
            this.cache = cache;
            this.lockFactory = lockFactory;
            this.httpClientFactory = httpClientFactory;
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            logger?.LogInformation($"Remote IpAddress: {context.HttpContext.Connection.RemoteIpAddress}");

            var remoteIp = context.HttpContext.Connection.RemoteIpAddress;
            logger?.LogDebug($"Request from Remote IP address: {remoteIp}");

            if (remoteIp.Equals(IPAddress.Loopback))
            {
                base.OnActionExecuting(context);
                return;
            }

            var key = WechatCacheKeyEnum.SERVER_ADDRESS.ToRedisKey();
            var check_result = false;
            try
            {
                slim.EnterReadLock();
                if (!cache.KeyExists(key))
                {
                    slim.ExitReadLock();
                    slim.EnterWriteLock();
                    try
                    {
                        using (var redLock = lockFactory.CreateLock(key, TimeSpan.FromSeconds(5)))
                        {
                            if (redLock.IsAcquired)
                            {
                                var client = httpClientFactory.CreateClient();
                                //https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=ACCESS_TOKEN
                                WechatContext.Instance.GetAccessTokenAsync()
                                    .ContinueWith(pt => client.GetAsync("https://api.weixin.qq.com/cgi-bin/getcallbackip", new KeyValuePair<string, string>("access_token", pt.Result))
                                        .ContinueWith(pt0 => pt0.IsFaulted ? Task.CompletedTask : pt0.Result.Content.ReadAsStringAsync()
                                            .ContinueWith(pt1 => Task.Run(() =>
                                            {
                                                logger?.LogDebug($"微信服务器地址白名单刷新{pt1.Result}");
                                                var result = JsonConvert.DeserializeObject<WsrmServerAddressList>(pt1.Result);
                                                result.ThrowException();
                                                cache.KeyDelete(key);
                                                foreach (var t in result.ip_list)
                                                {
                                                    cache.SetAdd(key, t);
                                                }
                                                cache.KeyExpire(key, TimeSpan.FromDays(1));
                                            }))))
                                    .Wait();
                            }
                        }
                    }
                    finally
                    {
                        slim.ExitWriteLock();
                        slim.EnterReadLock();
                    }

                }

                check_result = cache.SetContains(key, $"{remoteIp}");
            }
            finally
            {
                slim.ExitReadLock();
            }

            if (check_result)
            {
                base.OnActionExecuting(context);
            }
            else
            {
                context.Result = new StatusCodeResult(401);
                return;
            }
        }

    }
}

namespace Tsual.Wechat.Controllers
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class GenericControllerNameConventionAttribute : Attribute, IControllerModelConvention
    {
        public void Apply(ControllerModel controller)
        {
            if (controller.ControllerType.GetGenericTypeDefinition() !=
                typeof(TsualWechatController<>))
            {
                // Not a GenericController, ignore.
                return;
            }

            var entityType = controller.ControllerType.GenericTypeArguments[0];
            controller.ControllerName = entityType.Name;
        }
    }

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
    public class TsualRouteAttribute : Attribute, IRouteTemplateProvider
    {
        public string Template => WechatContext.Instance.ControllerRoute;

        public int? Order { get; set; }

        public string Name { get; set; }
    }


    [TsualRoute]
    [GenericControllerNameConvention]
    public class TsualWechatController<T> : Controller
    {
        ILogger<TsualWechatController<T>> logger;
        IServiceProvider serviceProvider;

        public TsualWechatController(ILogger<TsualWechatController<T>> logger, IServiceProvider serviceProvider)
        {
            this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
            this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
        }

        [HttpGet]
        [ServiceFilter(typeof(WechatServerAddressFilter))]
        public IActionResult TokenVertify(string signature, string timestamp, string nonce, string echostr) => WechatContext.Instance.GetSignature(timestamp, nonce) == signature ? Content(echostr) : Content("fail at check signature");

        [HttpPost]
        [ServiceFilter(typeof(WechatServerAddressFilter))]
        public async Task<IWechatMessageHandleResult> MessagePostAsync([FromBody]IWechatMessageRequestModel model)
        {
            logger?.LogTrace("接收到微信服务器post消息{0}=>{1}", JsonConvert.SerializeObject(Request.Headers), JsonConvert.SerializeObject(model));
            return await WechatContext.Instance.MessageMaster.HandleMessageAsync(model, serviceProvider, logger);
        }
    }

    public class WechatControllerProvider : IApplicationFeatureProvider<ControllerFeature>
    {
        public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
        {
            if (!feature.Controllers.Any(t => t.Name == typeof(TsualWechatController<>).Name))
            {
                var ctrl_type = typeof(TsualWechatController<>);
                feature.Controllers.Add(typeof(TsualWechatController<>).MakeGenericType(typeof(object)).GetTypeInfo());
            }
        }
    }

}


namespace Tsual.Wechat.Backgroud
{
    public abstract class WechatHostedService : IHostedService, IDisposable
    {
        IDatabase cache;
        IHttpClientFactory httpClientFactory;
        ILogger<WechatHostedService> logger;
        RedLockFactory lockFactory;

        public WechatHostedService(IDatabase cache, IHttpClientFactory httpClientFactory, ILogger<WechatHostedService> logger, RedLockFactory lockFactory)
        {
            this.cache = cache ?? throw new ArgumentNullException(nameof(cache));
            this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
            this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
            this.lockFactory = lockFactory ?? throw new ArgumentNullException(nameof(lockFactory));
        }

        Timer timer;
        abstract protected TimeSpan dueTime { get; }
        abstract protected TimeSpan period { get; }
        abstract protected string ServiceName { get; }

        public class CallBackData
        {
            public IDatabase cache { get; internal set; }
            public HttpClient httpClient { get; internal set; }
            public ILogger logger { get; internal set; }
            public RedLockFactory lockFactory { get; internal set; }
        }

        private CallBackData Create() => new CallBackData()
        {
            cache = cache,
            httpClient = httpClientFactory.CreateClient(),
            logger = logger,
            lockFactory = lockFactory
        };


        public void Dispose()
        {
            logger?.LogDebug($"微信后台任务{ServiceName}退出");
            timer?.Dispose();
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            logger?.LogDebug($"微信后台任务{ServiceName}开始");

            timer = new Timer(async obj => await DoWork(Create()), null, dueTime, period);

            return Task.CompletedTask;
        }

        protected abstract Task DoWork(CallBackData data);

        public Task StopAsync(CancellationToken cancellationToken)
        {
            logger?.LogDebug($"微信后台任务{ServiceName}终止");

            timer?.Change(Timeout.Infinite, 0);

            return Task.CompletedTask;
        }
    }
}

namespace Tsual.Wechat
{
    public interface ITsualWechatOption
    {
        string AesEncryptKey { get; }
        string AppId { get; }
        string AppSecret { get; }
        string CacheHead { get; }
        string ControllerRoute { get; }
        string ResponseTextError { get; }
        string ResponseTextNotfound { get; }
        string ResponseTextTimeout { get; }
        string Token { get; }
    }

    public class TsualWechatOption : IOptions<TsualWechatOption>, ITsualWechatOption
    {
        public TsualWechatOption Value => this;

        public string AppId { get; set; }
        public string AppSecret { get; set; }
        public string AesEncryptKey { get; set; }
        public string Token { get; set; }
        public string ControllerRoute { get; set; } = "wechat";
        public string CacheHead { get; set; } = "DEFAULT";

        public bool UseDefaultResponseText { get; set; } = true;
        public string ResponseTextTimeout { get; set; }
        public string ResponseTextNotfound { get; set; }
        public string ResponseTextError { get; set; }
    }

    public interface IWechatContextProvider
    {
        WechatContext Context { get; }
    }

    internal class DefaultWechatContextProvider : IWechatContextProvider
    {
        public WechatContext Context => WechatContext.Instance;

        public DefaultWechatContextProvider(IOptions<TsualWechatOption> setupAction, IServiceProvider serviceProvider)
        {
            if (setupAction is null)
            {
                throw new ArgumentNullException(nameof(setupAction));
            }

            if (serviceProvider is null)
            {
                throw new ArgumentNullException(nameof(serviceProvider));
            }

            var value = setupAction.Value;

            WechatContext._Instance.Configure(
                    serviceProvider: serviceProvider,
                    appId: value.AppId,
                    appSecret: value.AppSecret,
                    token: value.Token,
                    cacheHead: value.CacheHead,
                    aesEncryptKey: value.AesEncryptKey,
                    controllerRoute: value.ControllerRoute,
                    txtError: value.UseDefaultResponseText ? null : value.ResponseTextError,
                    txtTimeout: value.UseDefaultResponseText ? null : value.ResponseTextTimeout,
                    txtNotfound: value.UseDefaultResponseText ? null : value.ResponseTextNotfound

                );
        }
    }
}